#! /usr/bin/env python
#
# RTEMS (http://www.rtems.org/)
# Copyright 2020, 2022 Chris Johns (chrisj@rtems.org)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

from __future__ import print_function

import argparse
import os
import os.path
import re
import sys

rtems_version = 6


_BUILD_TYPE_BSP = re.compile(r"build-type:\s*bsp\n")
_ARCH = re.compile(r"arch:\s*(\S+)\n")
_FAMILY = re.compile(r"family:\s*(\S+)\n")
_BSP = re.compile(r"bsp:\s*(\S+)\n")


class ArchBsps:
    """Collects and processes the BSPs for a range of architectures
    creating output in text, markdown and HTML ir pandoc is installed"""
    def __init__(self, path='.', trace=False):
        self.trace = trace
        self._output = []
        self.top = os.path.realpath(path)
        self.base = os.path.join(self.top, 'spec', 'build', 'bsps')
        self.configs = []
        self.archs = {}
        self._collect('.yml')
        self._process()

    def _clear(self):
        """Clears the output."""
        self._output = []

    def _out(self, line=''):
        """Output a line to the output buffer."""
        if isinstance(line, list):
            self._output += line
        else:
            self._output += [line]

    def _collect(self, ext):
        """Collect the config files from the source tree."""
        self.configs = []
        for root, dirs, files in os.walk(self.base, topdown=True):
            for f in files:
                if os.path.splitext(f)[1] == ext:
                    self.configs += [os.path.join(root, f)]

    def _process(self):
        """Process the collected list of config files."""
        self.archs = {}
        for cfg in self.configs:
            with open(cfg, 'r') as cfg_file:
                content = cfg_file.read()
                if _BUILD_TYPE_BSP.search(content):
                    arch = _ARCH.search(content).group(1)
                    family = _FAMILY.search(content).group(1)
                    bsp = _BSP.search(content).group(1)
                    self.archs.setdefault(arch, {})
                    self.archs[arch].setdefault(family, {})
                    self.archs[arch][family][bsp] = cfg[len(self.base) + 1:]

    def _max_arch_len(self):
        """Finds the longest arch label"""
        maxlen = 0
        for arch in self.archs:
            if len(arch) > maxlen:
                maxlen = len(arch)
        return maxlen

    def _max_family_len(self):
        """Finds the longest family label"""
        maxlen = 0
        for arch in self.archs:
            for family in self.archs[arch]:
                if len(family) > maxlen:
                    maxlen = len(family)
        return maxlen

    def _max_bsp_len(self):
        """Finds the longest BSP label"""
        maxlen = 0
        for arch in self.archs:
            for family in self.archs[arch]:
                for bsp in self.archs[arch][family]:
                    if len(bsp) > maxlen:
                        maxlen = len(bsp)
        return maxlen

    def _max_bsp_path_len(self):
        """Finds the longest BSP path"""
        maxlen = 0
        for arch in self.archs:
            for family in self.archs[arch]:
                for bsp in self.archs[arch][family]:
                    if len(self.archs[arch][family][bsp]) > maxlen:
                        maxlen = len(self.archs[arch][family][bsp])
        return maxlen

    def title(self):
        """Returns the output's title"""
        return 'RTEMS %d Board Support Packages' % (rtems_version)

    def output(self):
        """Return the output"""
        return self._output

    def architectures(self):
        """Returns the number of architectures we have"""
        return len(self.archs)

    def families(self, arch=None):
        """Returns the number of BSP families we have for an architecture. If
        you supply an architecture the count is the families in the
        architure.

        """
        if arch is not None:
            return len(self.archs[arch])
        count = 0
        for arch in self.archs:
            count += len(self.archs[arch])
        return count

    def bsps(self, arch=None, family=None):
        """Returns the number of BSPs we have for an architecture or a family"""
        count = 0
        if arch is not None and family is not None:
            count = len(self.archs[arch][family])
        elif arch is None and family is not None:
            for arch in self.archs:
                if family in self.archs[arch]:
                    count = len(self.archs[arch][family])
                    break
        elif arch is not None and family is None:
            count = 0
            for family in self.archs[arch]:
                count += len(self.archs[arch][family])
        else:
            for arch in self.archs:
                for family in self.archs[arch]:
                    count += len(self.archs[arch][family])
        return count

    def text(self, arch_selector=None, family_selector=None, show_path=False):
        """Generate plain text output"""
        self._clear()
        self._out(self.title())
        self._out()
        self._out('Architectures: %d' % (self.architectures()))
        self._out('BSP Families: %d' % (self.families()))
        self._out('BSPs: %d' % (self.bsps()))
        max_family = self._max_family_len()
        max_bsp = self._max_bsp_len()
        if arch_selector is None:
            archs_matcher = []
        else:
            archs_matcher = [a.strip() for a in arch_selector.split(',')]
        if family_selector is None:
            family_matcher = []
        else:
            family_matcher = [f.strip() for f in family_selector.split(',')]
        for arch in sorted(self.archs.keys()):
            if arch_selector is None or arch in archs_matcher:
                first = True
                for family in sorted(self.archs[arch].keys()):
                    if family_selector is None or family in family_matcher:
                        if first:
                            self._out()
                            self._out('%s: (families:%d bsps:%d)' % \
                                      (arch,
                                       self.families(arch=arch),
                                       self.bsps(arch=arch)))
                            first = False
                        for bsp in sorted(self.archs[arch][family].keys()):
                            if show_path:
                                p = os.path.join('bsps',
                                                 self.archs[arch][family][bsp])
                                self._out(' %-*s %-*s %s' % \
                                          (max_bsp, bsp, max_family, family, p))
                            else:
                                self._out(' %-*s %s' % (max_bsp, bsp, family))

    def markdown(self,
                 arch_selector=None,
                 family_selector=None,
                 show_path=False,
                 show_title=False):
        """Generates markdown output"""
        self._clear()
        if show_title:
            self._out('# ' + self.title())
            self._out()
        self._out('**Architectures:** %d  ' % (self.architectures()))
        self._out('**BSP Families:** %d  ' % (self.families()))
        self._out('**BSPs:** %d  ' % (self.bsps()))
        max_arch = self._max_arch_len()
        max_family = self._max_family_len()
        max_bsp = self._max_bsp_len()
        max_bsp_path = self._max_bsp_path_len() + 4
        if arch_selector is None:
            archs_matcher = []
        else:
            archs_matcher = [a.strip() for a in arch_selector.split(',')]
        if family_selector is None:
            family_matcher = []
        else:
            family_matcher = [f.strip() for f in family_selector.split(',')]
        for arch in sorted(self.archs.keys()):
            if arch_selector is None or arch in archs_matcher:
                first = True
                for family in sorted(self.archs[arch].keys()):
                    if family_selector is None or family in family_matcher:
                        if first:
                            fbs = 'families:%-2d bsps:%-3d' % \
                                (self.families(arch=arch),
                                 self.bsps(arch=arch))
                            if max_family < len(fbs):
                                max_fb = len(fbs)
                            else:
                                max_fb = max_family
                            self._out()
                            if show_path:
                                self._out('%-*s |%-*s |' %
                                          (max_bsp, arch, max_fb, fbs))
                                self._out('%s-|%s-|-%s' %
                                          ('-' * max_bsp, '-' * max_fb,
                                           '-' * max_bsp_path))
                            else:
                                self._out('%-*s |%s' % (max_bsp, arch, fbs))
                                self._out('%s-|-%s' %
                                          ('-' * max_bsp, '-' * max_fb))
                            first = False
                        for bsp in sorted(self.archs[arch][family].keys()):
                            if show_path:
                                p = os.path.join('bsps',
                                                 self.archs[arch][family][bsp])
                                self._out('%-*s |%-*s |%s' % \
                                          (max_bsp, bsp, max_fb, family, p))
                            else:
                                self._out('%-*s |%s' % (max_bsp, bsp, family))

    def pairs(self, arch_selector=None, family_selector=None, show_path=False):
        """Generate output as pairs"""
        self._clear()
        max_arch = self._max_arch_len()
        max_bsp = self._max_bsp_len()
        if arch_selector is None:
            arch_matcher = []
        else:
            arch_matcher = [a.strip() for a in arch_selector.split(',')]
        if family_selector is None:
            family_matcher = []
        else:
            family_matcher = [f.strip() for f in family_selector.split(',')]
        for arch in sorted(self.archs.keys()):
            if arch_selector is None or arch in arch_matcher:
                for family in sorted(self.archs[arch].keys()):
                    if family_selector is None or family in family_matcher:
                        for bsp in sorted(self.archs[arch][family].keys()):
                            if show_path:
                                p = os.path.join('bsps',
                                                 self.archs[arch][family][bsp])
                                pair = arch + '/' + bsp
                                pair = '%-*s %s' % (max_arch + max_bsp + 1, pair, p)

                                self._out(pair)
                            else:
                                self._out('%s/%s' % (arch, bsp))

    def config(self, arch_selector=None, family_selector=None):
        """Generate output as pairs"""
        self._clear()
        self._out(['# Generated by rtems-bsp',
                   '[DEFAULT]',
                   '# Build',
                   'RTEMS_BUILD_LABEL = DEFAULT',
                   'RTEMS_DEBUG = False',
                   'RTEMS_PROFILING = False',
                   'RTEMS_POSIX_API = True',
                   '# Tests',
                   'BUILD_TESTS = False',
                   'BUILD_BENCHMARKS = False',
                   'BUILD_FSTESTS = False',
                   'BUILD_LIBTESTS = False',
                   'BUILD_MPTESTS = False',
                   'BUILD_PSXTESTS = False',
                   'BUILD_PSXTMTESTS = False',
                   'BUILD_RHEALSTONE = False',
                   'BUILD_SAMPLES = True',
                   'BUILD_SMPTESTS = False',
                   'BUILD_SPTESTS = False',
                   'BUILD_TMTESTS = False',
                   'BUILD_UNITTESTS = False',
                   'BUILD_VALIDATIONTESTS = False',
                   'RTEMS_TEST_VERBOSITY = Normal',
                   '# Compliler',
                   '; WARNING_FLAGS = -Wall',
                   '; CC_WARNING_FLAGS = -Wmissing-prototypes -Wimplicit-function-declaration -Wstrict-prototypes -Wnested-externs',
                   '; CXX_WARNING_FLAGS = ',
                   '; OPTIMIZATION_FLAGS = -O2 -g -fdata-sections -ffunction-sections',
                   '; BSP_OPTIMIZATION_FLAGS = ${OPTIMIZATION_FLAGS}',
                   '; CPUKIT_OPTIMIZATION_FLAGS = ${OPTIMIZATION_FLAGS}',
                   '; TEST_OPTIMIZATION_FLAGS = ${OPTIMIZATION_FLAGS}',
                   '; LINKFLAGS = ',
                   '; LDFLAGS = -Wl,--gc-sections',
                   '# BSP',
                   'BSP_VERBOSE_FATAL_EXTENSION = 1',
                   'BSP_PRINT_EXCEPTION_CONTEXT = 1',
                   'BSP_RESET_BOARD_AT_EXIT = 1'])
        self._out()
        max_arch = self._max_arch_len()
        max_bsp = self._max_bsp_len()
        if arch_selector is None:
            arch_matcher = []
        else:
            arch_matcher = [a.strip() for a in arch_selector.split(',')]
        if family_selector is None:
            family_matcher = []
        else:
            family_matcher = [f.strip() for f in family_selector.split(',')]
        for arch in sorted(self.archs.keys()):
            if arch_selector is None or arch in arch_matcher:
                for family in sorted(self.archs[arch].keys()):
                    if family_selector is None or family in family_matcher:
                        for bsp in sorted(self.archs[arch][family].keys()):
                            self._out('[%s/%s]' % (arch, bsp))
                            self._out()


def run(args):
    """Runs the command"""
    argsp = argparse.ArgumentParser(
        prog='rtems-bsps',
        description='List the BSP and architectures in RTEMS')
    argsp.add_argument('-a',
                       '--arch',
                       help='Output the BSPs in an architecture',
                       type=str,
                       default=None)
    argsp.add_argument('-f',
                       '--family',
                       help='Output the BSPs in an architecture family',
                       type=str,
                       default=None)
    argsp.add_argument('-p',
                       '--paths',
                       help='Show the BSP paths in the output',
                       action='store_true')
    argsp.add_argument('-m',
                       '--markdown',
                       help='Output list in markdown format',
                       action='store_true')
    argsp.add_argument('-T',
                       '--title',
                       help='Output a title in the markdown format',
                       action='store_true')
    argsp.add_argument('-v',
                       '--trace',
                       help='Verbose or trace for debugging',
                       action='store_true')
    argsp.add_argument('-P',
                       '--pairs',
                       help='Output architectures and BSPs in CPU/BSP format',
                       action='store_true')
    argsp.add_argument('-C',
                       '--config',
                       help='Output architectures and BSPs in `config.ini` format',
                       action='store_true')

    argopts = argsp.parse_args(args[1:])

    if argopts.arch is not None and argopts.family is not None:
        print('error: arch or family, not both at once', file=sys.stderr)
        sys.exit(1)

    ab = ArchBsps(trace=argopts.trace)

    if argopts.markdown:
        ab.markdown(arch_selector=argopts.arch,
                    family_selector=argopts.family,
                    show_path=argopts.paths,
                    show_title=argopts.title)
    elif argopts.pairs:
        ab.pairs(arch_selector=argopts.arch,
                 family_selector=argopts.family,
                 show_path=argopts.paths)
    elif argopts.config:
        ab.config(arch_selector=argopts.arch,
                 family_selector=argopts.family)
    else:
        ab.text(arch_selector=argopts.arch,
                family_selector=argopts.family,
                show_path=argopts.paths)

    print(os.linesep.join(ab.output()))


if __name__ == "__main__":
    run(sys.argv)
